D3.js(Draggable and Scalable Tree)

因为最近手上有个小的需求,设计一个可缩放和可拖拽的树形结构,我便去研读了D3官网给的一个树形的例子。

布局(Layout)

原本我以为理解了基本的选择器、元素操作、Enter、Exit就能去看实例的代码了,后来发现我错了,所以这里需要理解一下D3中布局(Layout)的概念。布局是D3中一个十分重要的概念,从布局衍生出很多图表。例如:饼状图(pie)、力导向图(force),树状图(tree)等等,基本实现了很多开源的可视化工具提供的图表。但是它又和很多可视化工具(如Echarts)有很大的不同。

相对于其它工具来说,D3较底层一点,所以初学者可能会觉得有点困难,但是一旦理解了D3布局的思想,使用起来,会比其它工具更加得心应手。首先,我阐释下D3和大部分可视化工具数据到图表的流程:

  • 大部分可视化工具:数据 => 封装好的绘图函数 => 图表
  • D3:数据 => Layout => 绘图所需的数据 => 绘制图形 => 图表

可以看出,D3需要自己去绘制图形,但是可以通过布局函数获得绘图所需要的数据,坏处是对初学者是一个很大的考验,好处是它能帮助我们制作出更加精密的图形。

树状图

回归正题,如何设计一个树形结构,我将从D3官网提供的示例代码分析。

页面代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>tree</title>
<style>
body{
margin: 0;
}
svg{
background-color: #eee;
}
.node circle {
cursor: pointer;
fill: #fff;
stroke: steelblue;
stroke-width: 1.5px;
}
.node text {
font-size: 11px;
}
path.link {
fill: none;
stroke: #ccc;
stroke-width: 1.5px;
}
g.detail rect{
fill: #000;
fill-opacity: .6;
rx: 5;
ry: 5;
}
g.detail text{
fill: #fff;
}
</style>
</head>
<body>
<div id="treeContainer"></div>
<script src="./dist/tree.bundle.js"></script>
</body>
</html>

因为D3示例代码是同步的形式读出整个树形数据结构,我对其进行了改造,模拟异步数据(async_city.json)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
{
"root": {
"name": "中国"
},
"中国": {
"name": "中国",
"children": [
{"name": "浙江"},
{"name": "广西"},
{"name": "黑龙江"},
{"name": "新疆"}
]
},
"浙江": {
"name": "浙江",
"children": [
{"name": "杭州"},
{"name": "宁波"},
{"name":"温州" },
{"name":"绍兴" }
]
},
"广西": {
"name": "广西",
"children": [
{"name": "桂林"},
{"name": "南宁"},
{"name": "柳州"},
{"name": "防城港"}
]
},
"桂林": {
"name": "桂林",
"children": [
{"name":"秀峰区"},
{"name":"叠彩区"},
{"name":"象山区"},
{"name":"七星区"}
]
},
"黑龙江": {
"name":"黑龙江",
"children":
[
{"name":"哈尔滨"},
{"name":"齐齐哈尔"},
{"name":"牡丹江"},
{"name":"大庆"}
]
},
"新疆" : {
"name":"新疆" ,
"children":
[
{"name":"乌鲁木齐"},
{"name":"克拉玛依"},
{"name":"吐鲁番"},
{"name":"哈密"}
]
}
}

画布

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var margin = {
top: 20,
left: 50,
right: 50,
bottom: 20
};
var width = $(document).width(),
height = $(document).height(),
i = 0,
limit = 2,
root;
// draggable and scalable
var zoomListener = d3.behavior.zoom().scaleExtent([0.1, 3]).on('zoom', zoom);
function zoom(){
d3.select('svg').select('g').attr('transform', 'translate(' + d3.event.translate + ')scale(' + d3.event.scale + ')');
}
var svg = d3.select("#treeContainer").append("svg")
.attr("width", width - margin.left - margin.right)
.attr("height", height - margin.top - margin.bottom)
.call(zoomListener)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");

获取异步数据

1
2
3
4
5
6
7
// 异步获取数据
function getData(sd, cb){
d3.json('data/async_city.json', function(err, json){
// 通过callback返回部分数据
cb && cb(json[sd.name]);
});
}

构造树

1
2
3
4
5
6
7
8
9
// 获取树的root
getData({name: 'root'}, function(json){
root = json;
root.x0 = height / 2;
root.y0 = width / 2;
// 初始化树根
update(root);
});

从上面的代码可以看出构造树的核心代码就是这个update函数,下面以注释的形式深入理解树形的构造。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
// 新建一个树的布局
var tree = d3.layout.tree()
.size([height - margin.top - margin.bottom, width - margin.left - margin.right]);
// 因为默认的树布局是自上而下的,这里构建一个自左向右的树,故需要一个转换x和y坐标的函数
var diagonal = d3.svg.diagonal()
.projection(function(d) { return [d.y, d.x]; });
function update(source) {
var duration = d3.event && d3.event.altKey ? 5000 : 500;
/**
* 这里实际上是通过tree的nodes函数获得树形结构的每个节点的数据,包括位置信息和深度
* 返回的数据结构如下:
* [{depth: 0, name: "中国", children: [], x: 380, y: 0}]
*/
var nodes = tree.nodes(root).reverse();
// 为了让当前节点居中,故更具当前节点的depth来计算各节点的y坐标(即横向位置)
var srcDepth = source.depth;
nodes.forEach(function(d){
d.y = height / 2 + 180 * (d.depth - srcDepth);
});
// Update the nodes…
var node = svg.selectAll("g.node")
.data(nodes, function(d) { return d.id || (d.id = ++i); });
// Enter any new nodes at the parent's previous position.
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) { return "translate(" + source.y0 + "," + source.x0 + ")"; })
.on("click", click)
.on('mouseover', function(d){
if(d.name == 'more') return;
// 鼠标hover某个节点时,显示一个详细信息的弹层
var detail = d3.select(this).append('g')
.attr('class', 'detail')
.attr('dx', d3.event.x)
.attr('dy', d3.event.y + 10);
detail.append('rect')
.attr('width', 100)
.attr('height', 100);
detail.append('text')
.attr('dx', '.35em')
.attr('dy', '2em')
.attr('text-anchor', 'start')
.text(function(d){
return 'name: ' + d.name;
});
})
.on('mousemove', function(d){
var detail = d3.select(this).select('.detail');
detail.attr('x', d3.event.x)
.attr('y', d3.event.y);
})
.on('mouseout', function(d){
if(d.name == 'more') return;
d3.select(this).select('.detail').remove();
});
nodeEnter.append("circle")
.attr("r", 1e-6)
.style("fill", function(d){ return !d.isExpand ? "lightsteelblue" : "#fff"; });
nodeEnter.append("text")
.attr("x", -10)
.attr("dy", ".35em")
.attr("text-anchor", "end")
.text(function(d) { return d.name; })
.style("fill-opacity", 1e-6);
// Transition nodes to their new position.
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; });
nodeUpdate.select("circle")
.attr("r", 10)
.style("fill", function(d){ return !d.isExpand ? "lightsteelblue" : "#fff"; });
nodeUpdate.select("text")
.style("fill-opacity", 1);
// Transition exiting nodes to the parent's new position.
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function(d) {
if(d.name == 'more') this.remove();
return "translate(" + source.y + "," + source.x + ")";
})
.remove();
nodeExit.select("circle")
.attr("r", 1e-6);
nodeExit.select("text")
.style("fill-opacity", 1e-6);
/** Update the links...
* tree.links方法获取连线节点之间的映射,返回的数据结构如下:
* [{source: {}, target: {}}]
*/
var link = svg.selectAll("path.link")
.data(tree.links(nodes), function(d) { return d.target.id; });
// Enter any new links at the parent's previous position.
link.enter().insert("path", "g")
.attr("class", "link")
.attr("d", function(d) {
var o = {x: source.x0, y: source.y0};
return diagonal({source: o, target: o});
})
.transition()
.duration(duration)
.attr("d", diagonal);
// Transition links to their new position.
link.transition()
.duration(duration)
.attr("d", diagonal);
// Transition exiting nodes to the parent's new position.
link.exit().transition()
.duration(duration)
.attr("d", function(d) {
if(d.target.name == 'more') this.remove();
var o = {x: source.x, y: source.y};
return diagonal({source: o, target: o});
})
.remove();
// Stash the old positions for transition.
// 记录当前节点所在的位置,为node update提供位移动画
nodes.forEach(function(d) {
d.x0 = d.x;
d.y0 = d.y;
});
}
function collapse(d){
delete d._children;
delete d.isExpand;
delete d.children;
}
function expand(d){
getData({name: d.name}, function(json){
if(json && json.children){
// 获取到此节点有子节点
d._children = json.children;
d.children = d._children.slice(0, limit);
if(d._children.length > d.children.length){
d.children.push({'name': 'more'});
}
}
d.isExpand = true;
update(d);
});
}
// 异步获取数据
function getData(sd, cb){
d3.json('data/async_city.json', function(err, json){
cb && cb(json[sd.name]);
});
}
function click(d){
if(d.name == 'more'){
// 点击更多
d.parent.children = d.parent._children.slice(0, (d.parent.children.length - 1) + limit);
if(d.parent._children.length > d.parent.children.length){
d.parent.children.push({'name': 'more'});
}
update(d.parent);
}else if(d.isExpand && d.children){
// 点击展开的节点
collapse(d);
update(d);
}else{
// 点击未展开的点
expand(d);
}
}

可以从https://github.com/brucewar/practice-in-D3获取示例代码

Enjoy it? Donate me!您的支持将鼓励我继续创作!

热评文章